var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var gIOService = Cc["@mozilla.org/network/io-service;1"].
                 getService(Ci.nsIIOService);
var gFeedContentTypes = ["application/atom+xml", "application/rss+xml", 
                         "appication/xml"];
var gProxyHosts = ["feedproxy.google.com", "feeds.feedburner.com",
                   "http://feeds2.feedburner.com/"];
// these are the proxies that hide the URIs of the articles. very very bad
// thing
var FEED_PROCESSOR_COM = "@code.google.com/on-the-wave/feed-processor;1";
// a hacked(less than 10 lines) version of the original feed processor
// see components/feedProcessor.js for the changes, you might want to set
// var FEED_PROCESSOR_COM = "@mozilla.org/feed-processor;1" 
// if the above component is unavailable

var FeedParser = Components.Constructor(FEED_PROCESSOR_COM, 
                                        "nsIFeedProcessor");
Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
String.prototype.startswith = function startswith(aOther){
  return this.substr(0, aOther.length) === aOther;
}

function FeedAggregateListener(aFeedURI, aStore){
  this.feedURI = aFeedURI;
  this.store = aStore;
}
FeedAggregateListener.prototype = {
  // nsIFeedResultListener
  handleResult: function handleResult(aResult){
    var kb = this.store;
    var feed = aResult.doc; //nsIFeed
    var feedDoc = kb.sym(this.feedURI); // a uri symbol representing
                                        // the source of the statements
    function SIOC(aTerm) 
      kb.sym("http://rdfs.org/sioc/ns#" + aTerm);
    function DCTERMS(aTerm) 
      kb.sym("http://purl.org/dc/terms/" + aTerm);
    function BBS(aTerm) 
      kb.sym("http://code.google.com/p/on-the-wave/voc#" + aTerm);
    function RDFS(aTerm) 
      kb.sym("http://www.w3.org/2000/01/rdf-schema#" + aTerm);
    function CONTENT(aTerm) 
      kb.sym("http://purl.org/rss/1.0/modules/content/" + aTerm);
    function FOAF(aTerm)
      kb.sym("http://xmlns.com/foaf/0.1/" + aTerm);

    if (!(feed instanceof Ci.nsIFeed))
      throw "handleResult: feed is not a nsIFeed";
    if (!feed.link)
      throw "handleResult: feed.link is empty";
    var originalArticle = kb.any(undefined,
                                 RDFS('seeAlso'), 
                                 feedDoc);//@seeAlso logic 
    if (originalArticle){
      var isBoard = false; //this is a feed of comments on an article
    } else {
      var isBoard = true; // this is a normal feed
      var theBoard = kb.sym(feed.link.spec);
    }

    if (isBoard){
      var boardOwners = feed.authors;
      for (var i = 0; i < boardOwners.length; i++){
        var person = boardOwners.queryElementAt(0, Ci.nsIFeedPerson);
        if (!person)
          throw "handleResult: author["+ i +"] is empty";
        kb.add(theBoard, SIOC('has_owner'), kb.literal(person.name),
               feedDoc);
      }
      
      if (!feed.title)
        throw "handleResult: feed.title is empty";
      kb.add(theBoard, DCTERMS('title'), 
             kb.literal(feed.title.plainText()), feedDoc);
    }

    //articles
    for (var i = 0; i < feed.items.length; i++){
      var entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);

      // CHECKPOINT: URI of an article
      // Ways of getting the URI of the articles:
      // 1.
      // var article = kb.sym(entry.link.spec);
      // but it is simply not correct if the publisher uses the god-damned
      // FeedBurner, even if the redirection is traced
      // 2. Use foaf:primaryTopic inference, but it's too hacky
      // 3. (see following)
      var content = entry.content || entry.summary;
      var fields = entry.fields;
      if (!content) 
        throw "entry " + i + " has no content, is this a feed?";
      
      if (bagHasKey(fields, 'origLinks')){
        var array = fields.getPropertyAsInterface('origLinks',
                                                  Ci.nsIArray);
        var uri = findValidLinkUri(array);
        var article = kb.sym(uri);
      } else if (entry.link && 
                 gProxyHosts.indexOf(entry.link.host) === -1){ //1.
        var article = kb.sym(entry.link.spec);        
      } else {
        var article = kb.sym(content.base.spec); //this is hacky
        dump("get URI of an article from content.base: " + article.uri +
             "\n");
      }        
      //throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
      //var article = kb.bnode();
     
      if (isBoard) {
        kb.add(article, SIOC('has_container'), theBoard, feedDoc);
      } else {
        //find what this article replies to 
        if (bagHasKey(fields, 'in-reply-to')){
          var inReplyBag = fields.getPropertyAsInterface('in-reply-to',
                                                     Ci.nsIPropertyBag2);
          //@ understand what 'ref' is for
          kb.add(kb.sym(inReplyBag.getPropertyAsAString('href')),
                 SIOC('has_reply'), article, feedDoc);
        } else {
          kb.add(originalArticle, SIOC('has_reply'), article, feedDoc);
        }        
      }

      //This part tries to find, if any, the rel="replies" link in an
      //ATOM feed
      var theFeedLinkAttributes = null; 
      // the attributes of the <link> element that                               // links to the feed of the replies
      if (bagHasKey(fields, 'links')){
        var array = fields.getPropertyAsInterface('links',
                                                  Ci.nsIArray);
        var repliesLinks = findAtomLinks('replies', array);

        for (var j = 0; j < repliesLinks.length; j++){ 
          //j!! be aware that JavaScript is function-scope based
          var attributes = repliesLinks[j];
          if (bagHasKey(attributes, 'type')) {            
            if (gFeedContentTypes.
                indexOf(attributes.getProperty('type')) !== -1)
              theFeedLinkAttributes = attributes;
          }
        }
      } // end of bagHasKey
 
      //CHECKPOINT: comment count
      //@@ should we count comments instead of relying on pre-count?
      //score of the articles based on the replies
      if (theFeedLinkAttributes){
        if (bagHasKey(theFeedLinkAttributes, 'thr:count'))
          kb.add(article, BBS('score'), 
                 theFeedLinkAttributes.getPropertyAsInt32('thr:count'), 
                 feedDoc);
      }
      if (bagHasKey(fields, 'slash:comments'))
        kb.add(article, BBS('score'), 
               fields.getPropertyAsInt32('slash:comments'), feedDoc);

      //CHECKPOINT: comment feed link  
      if (theFeedLinkAttributes)
        kb.add(article, RDFS('seeAlso'), 
               kb.sym(theFeedLinkAttributes.getProperty('href')), 
               feedDoc); 
      // it must have a href because findAtomLinks filtered it
      if (bagHasKey(fields, 'wfw:commentRss'))
        kb.add(article, RDFS('seeAlso'), 
               kb.sym(fields.getProperty('wfw:commentRss')), feedDoc);

      kb.add(article, DCTERMS('created'),
             W3CDTF_create(new Date(entry.published)), feedDoc);

      if (entry.authors.length){ //@only one now
        var person = entry.authors.queryElementAt(0, Ci.nsIFeedPerson);
        if (person && person.name) {
          var user = kb.bnode();
          kb.add(user, FOAF('accountName'), person.name, feedDoc);
          kb.add(article, SIOC('has_creator'), user, feedDoc);
        }
      }
      if (entry.title)
        kb.add(article, DCTERMS('title'), 
               entry.title.plainText(), feedDoc);
      kb.add(article, CONTENT('encoded'), content.text, feedDoc);
      //@might this contain relative URIs?
      
    } // article loop

  },
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedResultListener])
};

function parseFeedFromXhr(aXhr, aStore){
  var parser = new FeedParser();
  var feedURI = aXhr.uri.uri;
  parser.listener = new FeedAggregateListener(feedURI, aStore);
  parser.parseFromString(aXhr.responseText, 
                         gIOService.newURI(feedURI, null, null));
};

//utility functions
function resovleRedirectURI(){}

//<http://mxr.mozilla.org/mozilla-central/source/toolkit/components/feeds/src/FeedProcessor.js#bagHasKey>
function bagHasKey(bag, key) { 
  try {
     bag.getProperty(key);
     return true;
  }
  catch (e) {
    return false;
  }
}

//<http://mxr.mozilla.org/mozilla-central/source/toolkit/components/feeds/src/FeedProcessor.js#findAtomLinks>
const IANA_URI = "http://www.iana.org/assignments/relation/";
function findAtomLinks(rel, links) {
  var rvLinks = [];
  for (var i = 0; i < links.length; ++i) {
    var linkElement = links.queryElementAt(i, Ci.nsIPropertyBag2);
    // atom:link MUST have @href
    if (bagHasKey(linkElement, "href")) {
      var relAttribute = null;
      if (bagHasKey(linkElement, "rel"))
        relAttribute = linkElement.getPropertyAsAString("rel")
          if ((!relAttribute && rel == "alternate")||relAttribute == rel) {
            rvLinks.push(linkElement);
            continue;
          }
      // catch relations specified by IANA URI 
      if (relAttribute == IANA_URI + rel) {
        rvLinks.push(linkElement);
      }
    }
  }
  return rvLinks;
}

//find the non-proxy link from origLinks
/* feedburner:origLink is used in, for example, Google Official Blog and 
   mmdays, in which multiple feedburner:origLink are used for the same entry
   (with a fake one) */
function findValidLinkUri(links){
  if (links instanceof Ci.nsIArray){
    for (var i = 0; i < links.length; i++) {
      var links = links.QueryInterface(Ci.nsIArray);
      var candidateUri = links.queryElementAt(i, Ci.nsISupportsString).data;
      if (gProxyHosts.every(function(x) !candidateUri.startswith(x)))
        return candidateUri;
    }
    dump("findValidLinkUri: falling back with a bad URI: " + candidateUri +
         "\n");
    return candidateUri; //fallback, give a bad uri
  } else {
    throw TypeError("expecting nsIArray");
  }
}
//modified from <resource://gre/modules/ISO8601DateUtils.jsm>
function W3CDTF_create(aDate){
  function zeropad (s, l) {
    s = s.toString(); // force it to a string
    while (s.length < l) {
      s = '0' + s;
    }
    return s;
  }
  var result = zeropad(aDate.getUTCFullYear (), 4) +
               zeropad(aDate.getUTCMonth () + 1, 2) +
               zeropad(aDate.getUTCDate (), 2) + 'T' +
               zeropad(aDate.getUTCHours (), 2) + ':' +
               zeropad(aDate.getUTCMinutes (), 2) + ':' +
               zeropad(aDate.getUTCSeconds (), 2) + 'Z';

  return result;
}
